/**
 * Copyright (C) 2013- Iordan Iordanov
 *
 * This is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 3 of the License, or
 * (at your option) any later version.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this software; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307,
 * USA.
 */


package com.undatech.opaque.input;

import java.util.LinkedList;
import java.util.Queue;

import android.os.SystemClock;
import android.os.Vibrator;
import android.view.GestureDetector;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.ScaleGestureDetector;

import com.undatech.opaque.Constants;
import com.undatech.opaque.RemoteCanvas;
import com.undatech.opaque.RemoteCanvasActivity;
import com.undatech.opaque.input.RemotePointer;

abstract class InputHandlerGeneric extends GestureDetector.SimpleOnGestureListener 
										   implements InputHandler, ScaleGestureDetector.OnScaleGestureListener {
	private static final String TAG = "InputHandlerGeneric";

	protected GestureDetector gestureDetector;
	protected MyScaleGestureDetector scalingGestureDetector;

	// Handles to the RemoteCanvas view and RemoteCanvasActivity activity.
	protected RemoteCanvas canvas;
	protected RemoteCanvasActivity activity;
	protected PanRepeater panRepeater;
	
	// Used to generate haptic feedback
    protected Vibrator myVibrator;
	
	// This is the initial "focal point" of the gesture (between the two fingers).
	float xInitialFocus;
	float yInitialFocus;
	
	// This is the final "focal point" of the gesture (between the two fingers).
	float xCurrentFocus;
	float yCurrentFocus;
	float xPreviousFocus;
	float yPreviousFocus;
	
	// These variables record whether there was a two-finger swipe performed up or down.
	boolean inSwiping   = false;
	boolean scrollUp    = false;
	boolean scrollDown  = false;
	boolean scrollLeft  = false;
	boolean scrollRight = false;
	
	// These variables indicate whether the dpad should be used as arrow keys
	// and whether it should be rotated.
	boolean useDpadAsArrows    = false;
	boolean rotateDpad         = false;
	
	// The variables which indicates how many scroll events to send per swipe 
	// event and the maximum number to send at one time.
	long      swipeSpeed    = 1;
	final int maxSwipeSpeed = 7;
	
	// If swipe events are registered once every baseSwipeTime miliseconds, then
	// swipeSpeed will be one. If more often, swipe-speed goes up, if less, down.
	final long    baseSwipeTime = 400;
	
	// This is how far the swipe has to travel before a swipe event is generated.
	final float   baseSwipeDist = 40.f;
	
    // This is how far from the top and bottom edge to detect immersive swipe.
    final float   immersiveSwipeDistance = 50.f;
    boolean immersiveSwipe = false;
    
	// Some variables indicating what kind of a gesture we're currently in or just finished.
	boolean inScrolling         = false;
	boolean inScaling           = false;
	boolean scalingJustFinished = false;
	
	// The minimum distance a scale event has to traverse the FIRST time before scaling starts.
	final double  minScaleFactor = 0.1;
	
	// What action was previously performed by a mouse or stylus.
	int prevMouseOrStylusAction = 0;
	
	// Various drag modes in which we don't detect gestures.
	protected boolean panMode        = false;
	protected boolean dragMode       = false;
	protected boolean rightDragMode  = false;
	protected boolean middleDragMode = false;
	protected float   dragX, dragY;
	protected boolean singleHandedGesture = false;
	protected boolean singleHandedJustEnded = false;
	
	// These variables keep track of which pointers have seen ACTION_DOWN events.
	protected boolean secondPointerWasDown = false;
	protected boolean thirdPointerWasDown  = false;
	
    // What the display density is.
    float displayDensity = 0;
    
    // Indicates that the next onFling will be disregarded.
    boolean disregardNextOnFling = false;
    
    // Queue which holds the last two MotionEvents which triggered onScroll
    Queue<Float> distXQueue;
    Queue<Float> distYQueue;
    
	InputHandlerGeneric(RemoteCanvasActivity activity, RemoteCanvas canvas, Vibrator myVibrator) {
		this.activity = activity;
		this.canvas   = canvas;
		
		// TODO: Implement this
		useDpadAsArrows = true; //activity.getUseDpadAsArrows();
		rotateDpad      = false; //activity.getRotateDpad();
		
		gestureDetector = new GestureDetector (activity, this);
		scalingGestureDetector = new MyScaleGestureDetector (activity, this);
		
		gestureDetector.setOnDoubleTapListener(this);
		
	    this.myVibrator = myVibrator;
	    this.panRepeater = new PanRepeater (canvas, canvas.handler);
	    
	    displayDensity = canvas.getDisplayDensity();
	    
        distXQueue = new LinkedList<Float>();
        distYQueue = new LinkedList<Float>();
	}

	/**
	 * Function to get appropriate X coordinate from motion event for this input handler.
	 * @return the appropriate X coordinate.
	 */
	protected int getX (MotionEvent e) {
		float scale = canvas.getZoomFactor();
		return (int)(canvas.getAbsX() + e.getX() / scale);
	}

	/**
	 * Function to get appropriate Y coordinate from motion event for this input handler.
	 * @return the appropriate Y coordinate.
	 */
	protected int getY (MotionEvent e) {
		float scale = canvas.getZoomFactor();
		return (int)(canvas.getAbsY() + (e.getY() - 1.f * canvas.getTop()) / scale);
	}

	/**
	 * Handles actions performed by a mouse-like device.
	 * @param e touch or generic motion event
	 * @return
	 */
	protected boolean handleMouseActions (MotionEvent e) {
		boolean used     = false;
        final int action = e.getActionMasked();
        final int meta   = e.getMetaState();
		final int bstate = e.getButtonState();
        RemotePointer p   = canvas.getPointer();
		float scale      = canvas.getZoomFactor();
		int x = (int)(canvas.getAbsX() +  e.getX()                          / scale);
		int y = (int)(canvas.getAbsY() + (e.getY() - 1.f * canvas.getTop()) / scale);

		switch (action) {
		// If a mouse button was pressed or mouse was moved.
		case MotionEvent.ACTION_DOWN:
		case MotionEvent.ACTION_MOVE:
			switch (bstate) {
			case MotionEvent.BUTTON_PRIMARY:
		    	canvas.movePanToMakePointerVisible();
				p.leftButtonDown(x, y, meta);
				used = true;
				break;
			case MotionEvent.BUTTON_SECONDARY:
		    	canvas.movePanToMakePointerVisible();
				p.rightButtonDown(x, y, meta);
				used = true;
				break;
			case MotionEvent.BUTTON_TERTIARY:
		    	canvas.movePanToMakePointerVisible();
				p.middleButtonDown(x, y, meta);
				used = true;
				break;
			}
			break;
		// If a mouse button was released.
		case MotionEvent.ACTION_UP:
			switch (bstate) {
			case 0:
			    if (e.getToolType(0) != MotionEvent.TOOL_TYPE_MOUSE) {
			        break;
			    }
			case MotionEvent.BUTTON_PRIMARY:
			case MotionEvent.BUTTON_SECONDARY:
			case MotionEvent.BUTTON_TERTIARY:
		    	canvas.movePanToMakePointerVisible();
				p.releaseButton(x, y, meta);
				used = true;
				break;
			}
			break;
		// If the mouse wheel was scrolled.
		case MotionEvent.ACTION_SCROLL:
			float vscroll = e.getAxisValue(MotionEvent.AXIS_VSCROLL);
			float hscroll = e.getAxisValue(MotionEvent.AXIS_HSCROLL);
			scrollDown  = false;					
			scrollUp    = false;
			scrollRight = false;					
			scrollLeft  = false;
			// Determine direction and speed of scrolling.
			if (vscroll < 0) {
				swipeSpeed = (int)(-1*vscroll);
				scrollDown = false;
			} else if (vscroll > 0) {
				swipeSpeed = (int)vscroll;
				scrollUp   = false;
			} else if (hscroll < 0) {
				swipeSpeed = (int)(-1*hscroll);
				scrollRight = true;					
			} else if (hscroll > 0) {
				swipeSpeed = (int)hscroll;
				scrollLeft  = true;
			} else
				break;
			
			sendScrollEvents (x, y, meta);
			used = true;
			break;
		// If the mouse was moved OR as reported, some external mice trigger this when a
		// mouse button is pressed as well, so we check bstate here too.
		case MotionEvent.ACTION_HOVER_MOVE:
	    	canvas.movePanToMakePointerVisible();
			switch (bstate) {
			case MotionEvent.BUTTON_PRIMARY:
				p.leftButtonDown(x, y, meta);
				break;
			case MotionEvent.BUTTON_SECONDARY:
				p.rightButtonDown(x, y, meta);
				break;
			case MotionEvent.BUTTON_TERTIARY:
				p.middleButtonDown(x, y, meta);
				break;
			default:
				p.moveMouseButtonUp(x, y, meta);
				break;
			}
			used = true;
		}
		
		prevMouseOrStylusAction = action;
		return used;
	}

	/**
	 * Sends scroll events with previously set direction and speed.
	 * @param x
	 * @param y
	 * @param meta
	 */
	private void sendScrollEvents (int x, int y, int meta) {
        RemotePointer p = canvas.getPointer();
    	int numEvents = 0;
    	while (numEvents < swipeSpeed && numEvents < maxSwipeSpeed) {
    		if         (scrollDown) {
    			p.scrollDown(x, y, meta);
				p.moveMouseButtonUp(x, y, meta);
    		} else if (scrollUp) {
    			p.scrollUp(x, y, meta);
				p.moveMouseButtonUp(x, y, meta);
    		} else if (scrollRight) {
    			p.scrollRight(x, y, meta);
				p.moveMouseButtonUp(x, y, meta);
    		} else if (scrollLeft) {
    			p.scrollLeft(x, y, meta);
				p.moveMouseButtonUp(x, y, meta);
    		}
    		numEvents++;
    	}
	}
	
	/*
	 * @see android.view.GestureDetector.SimpleOnGestureListener#onSingleTapConfirmed(android.view.MotionEvent)
	 */
	@Override
	public boolean onSingleTapConfirmed(MotionEvent e) {
        RemotePointer p = canvas.getPointer();
        int metaState   = e.getMetaState();
		activity.showKbdIcon();
		p.leftButtonDown(getX(e), getY(e), metaState);
		SystemClock.sleep(50);
		p.releaseButton(getX(e), getY(e), metaState);
    	canvas.movePanToMakePointerVisible();
    	return true;
	}

	/*
	 * @see android.view.GestureDetector.SimpleOnGestureListener#onDoubleTap(android.view.MotionEvent)
	 */
	@Override
	public boolean onDoubleTap (MotionEvent e) {
        RemotePointer p = canvas.getPointer();
        int metaState   = e.getMetaState();
		p.leftButtonDown(getX(e), getY(e), metaState);
		SystemClock.sleep(50);
		p.releaseButton(getX(e), getY(e), metaState);
		SystemClock.sleep(50);
		p.leftButtonDown(getX(e), getY(e), metaState);
		SystemClock.sleep(50);
		p.releaseButton(getX(e), getY(e), metaState);
    	canvas.movePanToMakePointerVisible();
    	return true;
	}

	/*
	 * @see android.view.GestureDetector.SimpleOnGestureListener#onLongPress(android.view.MotionEvent)
	 */
	@Override
	public void onLongPress(MotionEvent e) {
        RemotePointer p = canvas.getPointer();
        int metaState   = e.getMetaState();

		// If we've performed a right/middle-click and the gesture is not over yet, do not start drag mode.
		if (secondPointerWasDown || thirdPointerWasDown)
			return;
		
		myVibrator.vibrate(Constants.SHORT_VIBRATION);

		dragMode = true;
		p.leftButtonDown(getX(e), getY(e), metaState);
	}

	/**
	 * Indicates that drag modes and scrolling have ended.
	 * @return
	 */
	protected boolean endDragModesAndScrolling () {
    	canvas.cursorBeingMoved = false;
		panMode               = false;
		inScaling             = false;
		inSwiping             = false;
		inScrolling           = false;
        immersiveSwipe        = false;
    	if (dragMode || rightDragMode || middleDragMode) {
    		dragMode          = false;
			rightDragMode     = false;
			middleDragMode    = false;
			return true;
    	} else {
    		return false;
    	}
	}

	/**
	 * Modify the event so that the mouse goes where we specify.
	 * @param e event to be modified.
	 * @param x new x coordinate.
	 * @param y new y coordinate.
	 */
	private void setEventCoordinates(MotionEvent e, float x, float y) {
		e.setLocation(x, y);
	}
    
    private void detectImmersiveSwipe (float y) {
        if (Constants.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT &&
            (y <= immersiveSwipeDistance || canvas.getHeight() - y <= immersiveSwipeDistance)) {
            inSwiping = true;
            immersiveSwipe = true;
        } else {
            inSwiping = false;
            immersiveSwipe = false;
        }
    }
    
	/*
	 * @see com.undatech.opaque.input.InputHandler#onTouchEvent(android.view.MotionEvent)
	 */
	@Override
	public boolean onTouchEvent(MotionEvent e) {
        final int action     = e.getActionMasked();
        final int index      = e.getActionIndex();
        final int pointerID  = e.getPointerId(index);
        final int meta       = e.getMetaState();
        RemotePointer p = canvas.getPointer();

        float f = e.getPressure();
        if (f > 2.f)
            f = f / 50.f;
        if (f > .92f) {
            disregardNextOnFling = true;
        }

        if (android.os.Build.VERSION.SDK_INT >= 14) {
	        // Handle and consume actions performed by a (e.g. USB or bluetooth) mouse.
	        if (handleMouseActions (e))
	        	return true;
        }

        if (action == MotionEvent.ACTION_UP) {
 			// Turn filtering back on and invalidate to make things pretty.
     		canvas.myDrawable.paint.setFilterBitmap(true);
     		canvas.invalidate();
        }

        switch (pointerID) {

        case 0:
        	switch (action) {
        	case MotionEvent.ACTION_DOWN:
        	    disregardNextOnFling = false;
        		singleHandedJustEnded = false;
                // We have put down first pointer on the screen, so we can reset the state of all click-state variables.
            	// Permit sending mouse-down event on long-tap again.
            	secondPointerWasDown = false;
            	// Permit right-clicking again.
            	thirdPointerWasDown = false;
            	// Cancel any effect of scaling having "just finished" (e.g. ignoring scrolling).
    			scalingJustFinished = false;
            	// Cancel drag modes and scrolling.
        		if (!singleHandedGesture) 
	            	endDragModesAndScrolling();
        		canvas.cursorBeingMoved = true;
        		// If we are manipulating the desktop, turn off bitmap filtering for faster response.
         		canvas.myDrawable.paint.setFilterBitmap(false);
         		// Indicate where we start dragging from.
         		dragX = e.getX();
    			dragY = e.getY();
                
                // Detect whether this is potentially the start of a gesture to show the nav bar.
                detectImmersiveSwipe(dragY);
    			break;
        	case MotionEvent.ACTION_UP:
        		singleHandedGesture = false;
        		singleHandedJustEnded = true;
                
                // If this is the end of a swipe that showed the nav bar, consume.
                if (immersiveSwipe && Math.abs(dragY - e.getY()) > immersiveSwipeDistance) {
                    endDragModesAndScrolling();
                    return true;
                }
                
    			// If any drag modes were going on, end them and send a mouse up event.
    			if (endDragModesAndScrolling()) {
    				p.releaseButton(getX(e), getY(e), meta);
    				return true;
    			}
    			break;
        	case MotionEvent.ACTION_MOVE:
                // Send scroll up/down events if swiping is happening.
                if (panMode) {
                	float scale = canvas.getZoomFactor();
            		canvas.relativePan(-(int)((e.getX() - dragX)*scale), -(int)((e.getY() - dragY)*scale));
        			dragX = e.getX();
        			dragY = e.getY();
        			return true;
                } else if (dragMode || rightDragMode || middleDragMode) {
                	canvas.movePanToMakePointerVisible();
        			p.moveMouseButtonDown(getX(e), getY(e), meta);
        			return true;
        		} else if (inSwiping) {
                	// Save the coordinates and restore them afterward.
                	float x = e.getX();
                	float y = e.getY();
                	// Set the coordinates to where the swipe began (i.e. where scaling started).
                	setEventCoordinates(e, xInitialFocus, yInitialFocus);
                	sendScrollEvents (getX(e), getY(e), meta);
                	// Restore the coordinates so that onScale doesn't get all muddled up.
                	setEventCoordinates(e, x, y);
        		} else if (immersiveSwipe) {
                    // If this is part of swipe that shows the nav bar, consume.
                    return true;
                }
        	}
        	break;

        case 1:
        	switch (action) {
        	case MotionEvent.ACTION_POINTER_DOWN:
        		// We re-calculate the initial focal point to be between the 1st and 2nd pointer index.
        		xInitialFocus = 0.5f * (dragX + e.getX(pointerID));
        		yInitialFocus = 0.5f * (dragY + e.getY(pointerID));
            	// Here we only prepare for the second click, which we perform on ACTION_POINTER_UP for pointerID==1.
    			endDragModesAndScrolling();
            	// Permit sending mouse-down event on long-tap again.
            	secondPointerWasDown = true;
            	// Permit right-clicking again.
            	thirdPointerWasDown  = false;
        		break;
        	case MotionEvent.ACTION_POINTER_UP:
        		if (!inSwiping && !inScaling && !thirdPointerWasDown) {
        	    	// If user taps with a second finger while first finger is down, then we treat this as
        	        // a right mouse click, but we only effect the click when the second pointer goes up.
        	        // If the user taps with a second and third finger while the first 
        	        // finger is down, we treat it as a middle mouse click. We ignore the lifting of the
        	        // second index when the third index has gone down (using the thirdPointerWasDown variable)
        	        // to prevent inadvertent right-clicks when a middle click has been performed.
        			p.rightButtonDown(getX(e), getY(e), meta);
	            	// Enter right-drag mode.
	            	rightDragMode = true;
	            	// Now the event must be passed on to the parent class in order to 
	            	// end scaling as it was certainly started when the second pointer went down.
        		}
        		break;
        	}
        	break;

        case 2:
        	switch (action) {
        	case MotionEvent.ACTION_POINTER_DOWN:
        		if (!inScaling) {
                	// This boolean prevents the right-click from firing simultaneously as a middle button click.
                	thirdPointerWasDown = true;
                	p.middleButtonDown(getX(e), getY(e), meta);
        			// Enter middle-drag mode.
                	middleDragMode      = true;
        		}
        	}
        	break;
        }
        
		scalingGestureDetector.onTouchEvent(e);
		return gestureDetector.onTouchEvent(e);
	}

	/*
	 * @see android.view.ScaleGestureDetector.OnScaleGestureListener#onScale(android.view.ScaleGestureDetector)
	 */
	@Override
	public boolean onScale(ScaleGestureDetector detector) {
		//android.util.Log.i(TAG, "onScale called");
		boolean eventConsumed = true;

		// Get the current focus.
		xCurrentFocus = detector.getFocusX();
		yCurrentFocus = detector.getFocusY();
		
		// If we haven't started scaling yet, we check whether a swipe is being performed.
		// The arbitrary fudge factor may not be the best way to set a tolerance...
		if (!inScaling) {
			// Start swiping mode only after we've moved away from the initial focal point some distance.
			if (!inSwiping) {
				if ( (yCurrentFocus < (yInitialFocus - baseSwipeDist)) ||
			         (yCurrentFocus > (yInitialFocus + baseSwipeDist)) ||
			         (xCurrentFocus < (xInitialFocus - baseSwipeDist)) ||
			         (xCurrentFocus > (xInitialFocus + baseSwipeDist)) ) {
					inSwiping      = true;
					xPreviousFocus = xCurrentFocus;
					yPreviousFocus = yCurrentFocus;
				}
			}
			
			// If in swiping mode, indicate a swipe at regular intervals.
			if (inSwiping) {
				scrollDown  = false;					
				scrollUp    = false;
				scrollRight = false;					
				scrollLeft  = false;
				if        (yCurrentFocus < (yPreviousFocus - baseSwipeDist)) {
					scrollDown     = true;
					xPreviousFocus = xCurrentFocus;
					yPreviousFocus = yCurrentFocus;
				} else if (yCurrentFocus > (yPreviousFocus + baseSwipeDist)) {
					scrollUp       = true;
					xPreviousFocus = xCurrentFocus;
					yPreviousFocus = yCurrentFocus;
				} else if (xCurrentFocus < (xPreviousFocus - baseSwipeDist)) {
					scrollRight    = true;
					xPreviousFocus = xCurrentFocus;
					yPreviousFocus = yCurrentFocus;
				} else if (xCurrentFocus > (xPreviousFocus + baseSwipeDist)) {
					scrollLeft     = true;
					xPreviousFocus = xCurrentFocus;
					yPreviousFocus = yCurrentFocus;
				} else {
					eventConsumed  = false;
				}
				// The faster we swipe, the faster we traverse the screen, and hence, the 
				// smaller the time-delta between consumed events. We take the reciprocal
				// obtain swipeSpeed. If it goes to zero, we set it to at least one.
				long elapsedTime = detector.getTimeDelta();
				if (elapsedTime < 10) elapsedTime = 10;
				
				swipeSpeed = baseSwipeTime/elapsedTime;
				if (swipeSpeed == 0)  swipeSpeed = 1;
				//if (consumed)        Log.d(TAG,"Current swipe speed: " + swipeSpeed);
			}
		}
		
		if (!inSwiping) {
			if ( !inScaling && Math.abs(1.0 - detector.getScaleFactor()) < minScaleFactor ) {
				//android.util.Log.i(TAG,"Not scaling due to small scale factor.");
				eventConsumed = false;
			}

			if (eventConsumed && canvas != null && canvas.canvasZoomer != null) {
				if (inScaling == false) {
					inScaling = true;
				}
				//android.util.Log.i(TAG, "Changing zoom level: " + detector.getScaleFactor());
				canvas.canvasZoomer.changeZoom(detector.getScaleFactor());
			}
		}
		return eventConsumed;
	}

	/*
	 * @see android.view.ScaleGestureDetector.OnScaleGestureListener#onScaleBegin(android.view.ScaleGestureDetector)
	 */
	@Override
	public boolean onScaleBegin(ScaleGestureDetector detector) {
		//android.util.Log.i(TAG, "onScaleBegin ("+xInitialFocus+","+yInitialFocus+")");
		inScaling           = false;
		scalingJustFinished = false;
		// Cancel any swipes that may have been registered last time.
		inSwiping   = false;
		scrollDown  = false;					
		scrollUp    = false;
		scrollRight = false;					
		scrollLeft  = false;
		return true;
	}

	/*
	 * @see android.view.ScaleGestureDetector.OnScaleGestureListener#onScaleEnd(android.view.ScaleGestureDetector)
	 */
	@Override
	public void onScaleEnd(ScaleGestureDetector detector) {
		//android.util.Log.i(TAG, "onScaleEnd");
		inScaling = false;
		inSwiping = false;
		scalingJustFinished = true;
	}
	
	/*
	 * @see com.undatech.opaque.input.InputHandler#onKeyDown(int, android.view.KeyEvent)
	 */
	@Override
	public boolean onKeyDown(int keyCode, KeyEvent evt) {
		return canvas.getKeyboard().keyEvent(keyCode, evt);
	}
	
	/*
	 * @see com.undatech.opaque.input.InputHandler#onKeyUp(int, android.view.KeyEvent)
	 */
	@Override
	public boolean onKeyUp(int keyCode, KeyEvent evt) {
		return canvas.getKeyboard().keyEvent(keyCode, evt);
	}

    /**
     * Returns the sign of the given number.
     * @param number
     * @return
     */
    protected float getSign (float number) {
        float sign;
        if (number >= 0) {
            sign = 1.f;
        } else {
            sign = -1.f;
        }
        return sign;
    }
}
